ANG Cyphers

One of my favorite things to do is make cyphers—programs that take data xyz and disguise it as abc. It is a fun exercise which uses a wide range of methods in Python.

Here I include two Ceasar Cyphers (Monoalphabetic), two brute force functions, three transposition cyphers, and one AES-like cypher. Are they as secure as actual AES or other types of modern encryption? No. These cyphers are not secure. Regardless, I did this project because it was interesting and I learned a lot while writing it.

I made it a point for myself to not look at any Python code that encrypts data while writing these. I wanted to write these before I looked at any actual encryption code. Although I did read a very interesting article that gave a general overview of how AES works. You can read it here: What is AES encryption and how does it work?



ANG Cypher Files:

I wrote four primary files to culminate the presentation of this project.

  1. run.py

  2. cypher_1_pyqt_prog_2_1.py

  3. adv_arr_cyphers.py

  4. ang_tools.py

You can download it here:
DOWNLOAD: ANG Cyphers
Or you can view the contents of the files on my github: Here.

I recommend playing around with it along with what I present here. I haven't written it into a GUI app yet but plan to in the future.

run.py:


# -*- coding: utf-8 -*-
"""
Created on Fri Jan 14 15:41:59 2022

@author: ANG
"""
import numpy as np
import glob 
import inspect 
np.set_printoptions( threshold=20, edgeitems=10, linewidth=119,)

'''
When importing explicit is better than implicit. So, here I import cypher_1_pyqt_prog_2_1
from cyphers. 
'''
from cyphers import cypher_1_pyqt_prog_2_1 as cypher_m #imports the file cypher_1_pyqt_prog_2_1.py from the dir cyphers
from cyphers import adv_arr_cyphers as advc

'''
Really good example of exploring the directory. 
'''
def show_direct():
    print('Current local scope:', *dir(), '\n', sep='\n')
    print('Current working directory:', *glob.glob('./**'), '\n', sep='\n')
    print('Sub Folder Content for Cyphers:', *glob.glob('./cyphers/**'), '\n', sep='\n') 
    print('All functions in imported modules:', 
          'cypher_m',
          *inspect.getmembers(cypher_m, inspect.isfunction),
          'advc',
          *inspect.getmembers(advc, inspect.isfunction),
          sep='\n')
    return
    
So when we run run.py we can use show_direct() to get a look at our current local scope, working directory, sub folders conent in ANG cyphers, and the functions that cypher data.

Looks something like this:

Python Console:
In[]: show_direct()

Current local scope:


Current working directory:
.\ang_cyphers.rar
.\cyphers
.\run.py
.\__init__.py


Sub Folder Content for Cyphers:
./cyphers\adv_arr_cyphers.py
./cyphers\ang_tools.py
./cyphers\cypher_1_pyqt_prog_2_1.py
./cyphers\NOTES.txt
./cyphers\__init__.py


All functions in imported modules:
cypher_m
('a_c_1', <function a_c_1 at 0x0000027BA90859D0>)
('a_c_2', <function a_c_2 at 0x0000027BA9085310>)
('a_c_3', <function a_c_3 at 0x0000027BA9085A60>)
('brute_force_0', <function brute_force at 0x0000027BA90853A0>)
('brute_force_1', <function brute_force_2 at 0x0000027BA90858B0>)
('c_c_3_1', <function c_c_3_1 at 0x0000027BA9085F70>)
('c_c_3_2', <function c_c_3_2 at 0x0000027BA9085940>)
('reshape_calc', <function reshape_calc at 0x0000027BA90854C0>)
advc
('a_d_1', <function a_d_1 at 0x0000027BA9085C10>)
('reshape_calc', <function reshape_calc at 0x0000027BA90854C0>)
('sim_aes_0', <function sim_aes_0 at 0x0000027BA9085040>)

'''
So, here we can see all the cyphers we can call to the console to scramble some 
string data. 

a_c_1 will encode or decode any printable characters and returns encoded data as 
a list of numbered index which gets scrambled. 

a_c_2 will encode or decode any printable characters and returns encoded data as 
a list of numbered index which gets scrambled a bit more than a_c_1. 


a_c_3 will encode or decode any printable characters and returns encoded data as 
a 3d numpy array. 

c_c_3_1 will encode or decode only lowercase ascii characters. 

c_c_3_2 will encode or decode any printable string characters. 

brute_force_0 will show all possible encodings of a given encyphered string 
that is only lowercase ascii—it only works on c_c_3_1. 

brute_force_1 will show all possible encodings of an encoded string of any 
printable strings—it only works on c_c_3_2. 

a_d_1 takes a string and a password and encodes the data based on the password and 
returns encoded string data as a 3d Numpy array. 

sim_aes_0 simulates AES encryption. It is not what AES encryption would actually 
look like but I tried to write it in that image. Where it: 
-> takes a string to encode and a 16 character key 
-> expands the key into 10 seperate keys 
-> converts the string to a bytearray 
-> changes values in the array based on position (substitutes)
-> scrambles the rows/cols 
-> applies a round key (the last three steps it does 10 times in that order) 
-> returns a list of cyphered data. 
'''
    


Simple Ceasar Cypher cypher_m.c_c_3_1():

I really enjoy reading about the history of cyphers too. One of the earliest known cyphers was the Ceasar Cypher. Julius Ceasar was known to send encoded messages he wanted to keep secret by shifting the letters of the alphabet. The traditional Ceasar Cypher uses a shift value of three.

Let's look at the two Ceasar Cyphers I've created here:


'''
Say Ceasar wants to order troops to attack some enemy at dawn. 
'''
In []: message_0 = cypher_m.c_c_3_1('attackdawn', enc=True) ; print(message_0)
Out[]: unnuwexuqh

'''
And to decode the message...
'''
In []: cypher_m.c_c_3_1(message_0, enc=False)
Out[]: 'attackdawn'

'''
Let us say we only had the encoded message, maybe we are an enemy that intercepted 
the message. 
'''
In []: some_enc_mes_0
Out[]: 'gipyvunnufcihnimionbyumn'

'''
We can brute force attack the message to decode it. 
'''
In []: cypher_m.brute_force_0(some_enc_mes_0)
Out[]: 
['gipyvunnufcihnimionbyumn',
 'hjqzwvoovgdjiojnjpoczvno',
 'ikraxwppwhekjpkokqpdawop',
 'jlsbyxqqxiflkqlplrqebxpq',
 'kmtczyrryjgmlrmqmsrfcyqr',
 'lnudazsszkhnmsnrntsgdzrs',
 'movebattaliontosoutheast',
 ...]
 '''
 Most of the output is just nonsense except for 'movebattaliontosoutheast'. Look 
 at that, we now know Ceasars troop movements. 
 '''
    

Simple Ceasar Cypher cypher_m.c_c_3_2():

But what if Ceasar used a larger alphabet—like all printable strings?


In []: message_1 = cypher_m.c_c_3_2('The enemey moved to (44.18N, 11.2E); prepare.', enc=True)
In []: message_1
Out[]: 'Nb8_8h8g8s_gip87_ni_"\x0b\x0b(\t2H&_\t\t(\ny#+_jl8j4l8('

'''
Looks more complicated right? But it is not. It is actually just as simple but uses 
a larger alphabet. We can easily brute force an encoded message just like the first. 
Say we intercepted the following:
'''
In []: intercepted_mes = 'unn46e_nliijm_4n_"\x0c\r(\x0cH&_\t\n(\ry#+_\n\x0b_Biolm_9lig_\n\n  _9lig_"\x0c\n(\tH&_\t\n(\ty'
In []: cypher_m.brute_force_1(intercepted_mes)
Out[]: 
['unn46e_nliijm_4n_"\x0c\r(\x0cH&_\t\n(\ry#+_\n\x0b_Biolm_9lig_\n\n  _9lig_"\x0c\n(\tH&_\t\n(\ty#',
 "voo57f`omjjkn`5o`#0\x0b)0I'`\n\r)\x0bz$,`\r\x0c`Cjpmn`amjh`\r\r\t\t`amjh`#0\r)\nI'`\n\r)\nz$",
 'wpp68g{pnkklo{6p{$1\x0c*1J({\r\x0b*\x0cA%-{\x0b0{Dkqno{bnki{\x0b\x0b\n\n{bnki{$1\x0b*\rJ({\r\x0b*\rA%',
 'xqq79h|qollmp|7q|%20+2K)|\x0b\x0c+0B&.|\x0c1|Elrop|colj|\x0c\x0c\r\r|colj|%2\x0c+\x0bK)|\x0b\x0c+\x0bB&',
 "yrr8ai}rpmmnq}8r}&31,3L*}\x0c0,1C'/}02}Fmspq}dpmk}00\x0b\x0b}dpmk}&30,\x0cL*}\x0c0,\x0cC'",
 "zss9bj~sqnnor~9s~'42-4M+~01-2D(:~13~Gntqr~eqnl~11\x0c\x0c~eqnl~'41-0M+~01-0D(",
 'Attack troops at (53.5N, 12.3E); 24 Hours from 2200 from (52.1N, 12.1E)',
 ...]
 
 '''
 Most of the output is just gibberish except for:
 'Attack troops at (53.5N, 12.3E); 24 Hours from 2200 from (52.1N, 12.1E)'

 Also the output is actually rather long and could be resource heavy depending on 
 the length of the message. Brute force is not a very good way to decode anything 
 but for our purpose here it works. 
 ''' 
    


More Complex Substitution Cyphers:

You can add more difficulty by adding extra steps. So, a_c_1, a_c_2, and a_c_3 are all substitution cyphers. Where a_c_1 and a_c_2 return the index of the letters, in the char_dict, as a list with some applied math operation and a_c_3 returns an array. In this case it is very simple.

cypher_m.a_c_1():


In []: message_2 = cypher_m.a_c_1('Move battalion to the south 39 Degrees.', enc=True)
In []: print(message_2)
Out[]: 
[52, 28, 35, 18, 98, 15, 14, 33, 33, 14, 25, 22, 28, 27, 98, 33, 28, 98, 33, 21, 
 18, 98, 32, 28, 34, 33, 21, 98, 7, 13, 98, 43, 18, 20, 31, 18, 18, 32, 79]

'''
This cypher is substitution-like in the sense that it returns the index of the character 
with an applied arithmatic—in this case it adds 3. This is not at all strong and 
may be partially deciphered with frequency analysis. It is at least easy to see 
a pattern. 

To decypher it subtracts three and returns the character at index. 

Say we aquired the following message encoded with a_c_1: 
'''
In []: message_2_1 = [44, 27, 18, 26, 38, 98, 14, 29, 29, 31, 28, 14, 16, 21, 18, 
                      32, 98, 19, 31, 28, 26, 98, 33, 21, 18, 98, 27, 28, 31, 33, 
                      21, 98, 94, 9, 4, 79, 6, 53, 77, 98, 5, 6, 79, 9, 44, 96, 98, 
                      79, 98, 52, 28, 35, 18, 98, 33, 28, 98, 22, 27, 33, 18, 31, 
                      16, 18, 29, 33, 79]
'''
We can pass it through the function to decode it. 
'''
In []: cypher_m.a_c_1(message_2_1, enc=False)
Out[]: 'Enemy approaches from the north {50.2N, 12.5E} . Move to intercept.'
    

cypher_m.a_c_2():


'''
a_c_2 does pretty much the same but applies arithmatic to different data based on 
position. 
Say we got message_2_1 encoded with a_c_2: 
'''
In []: message_2_1 = cypher_m.a_c_2('Enemy approaches from the north {50.2N, 12.5E} . Move to intercept.', enc=True)
In []: print(message_2_1)
Out[]: 
[36, 27, 10, 26, 30, 98, 6, 29, 21, 31, 20, 14, 8, 21, 10, 32, 90, 19, 23, 28, 18, 
  98, 25, 21, 10, 98, 19, 28, 23, 33, 13, 98, 86, 9, -4, 79, -2, 53, 69, 98, -3, 6, 
  71, 9, 36, 96, 90, 79, 90, 52, 20, 35, 10, 98, 25, 28, 90, 22, 19, 33, 10, 31, 8, 
  18, 21, 33, 71]
'''
You can see it is a little more scrambled and may be a little more difficult to 
decode if you did not know the alphabet used and steps taken. 
'''

cypher_m.a_c_3():


'''
a_c_3 does the same but returns a 3d numpy array. 
'''
In []: message_2_1 = cypher_m.a_c_3('Enemy approaches from the north {50.2N, 12.5E} . Move to intercept at dawn.', enc=True)
In []: print(message_2_1)
Out[]: 
[[[36 27 10 26 30 98  6 29 21 31 20 14  8 21 10]
  [32 90 19 23 28 18 98 25 21 10 98 19 28 23 33]
  [13 98 86  9 -4 79 -2 53 69 98 -3  6 71  9 36]
  [96 90 79 90 52 20 35 10 98 25 28 90 22 19 33]
  [10 31  8 18 21 33 90 14 25 98  9 14 28 27 71]]]

'''
This is where you can get really creative. You can rewrite a_c_3 to apply a bunch 
of different array methods to scramble the data before it returns the encyphered 
message. Think of it like a rubik's cube but with letters that are substituted as 
integers, that you can apply all kinds of mathamatic operations to. 
'''


adv_arr_cyphers.py:

These last cyphers in adv_arr_cyphers add some elements not in the ones above.

advc.a_d_1():


'''
a_d_1() add some complexity by requiring a password. If the password is wrong when 
trying to decypher the output it will not work. 
'''
In []: message = advc.a_d_1('some string to encode', 'passwrd', enc=True)
Out[]: 
array([[[-3179912117946028251,  3179912117946028303, -3179912117946028257,  3179912117946028293,
         -3179912117946028185,  3179912117946028307, -3179912117946028250],
        [ 3179912117946028306, -3179912117946028261,  3179912117946028302, -3179912117946028263,
          3179912117946028373, -3179912117946028250,  3179912117946028303],
        [-3179912117946028185,  3179912117946028293, -3179912117946028256,  3179912117946028291,
         -3179912117946028255,  3179912117946028292, -3179912117946028265]]], dtype=int64)
In []: advc.a_d_1(message, 'passwrd', enc=False)
Out[]: 'some string to encode' 
    

advc.sim_aes_0():


'''
I tried to simulate an AES encryption with this last cypher—so this is not actual 
AES but I tried to write it in that likeness. It is definitely the most complex 
of all the cyphers on this page. What advc.sim_aes_0 does are the following: 
-> takes a string to encode and a 16 character key (key must be 16 char)
-> expands the key into 10 seperate keys 
-> converts the string to a bytearray 
-> if the message len is not divisble by 16 it pads it with random data until it is
-> changes values in the array based on position (substitutes)
-> scrambles the rows/cols 
-> applies a round key (the last three steps it does 10 times in that order) 
-> returns a list of cyphered data. 
'''
In []: sim_mess_dec = advc.sim_aes_0(vals='This message is super secret', 
                                      key='K3%#ACHD^ddIo9)&', 
                                      enc=True)
In []: print(sim_mess_dec)
Out[]: 
[102, 244, 83, 91, 33, 39, 118, 243, 456, 206, 91, 99, 157, 212, 229, 176, 159, 
 242, 8, 143, 229, 119, 153, 232, 111, 146, 99, 11, 170, 210, 146, 213]

'''
To decode ...
'''
In []: advc.sim_aes_0(sim_mess_dec, key='K3%#ACHD^ddIo9)&', enc=False)
Out[]: 'This message is super secret<\x0b+%'

'''
So this cypher is not perfect because the padded random data is still at the end 
of the output. In the future I plan on fixing this as I continue to tinker with 
these things over time. 
'''